using System;
using System.IO;
using System.Runtime.Serialization;
using NuGet;
using Wolfpack.Core;
using Wolfpack.Core.Checks;
using Wolfpack.Core.Interfaces.Entities;
using Wolfpack.Core.Notification;

namespace Wolfpack.Contrib.Checks.NuGet
{
    /// <summary>
    /// This will check for a new release to a NuGet package.
    /// </summary>
    public class NuGetReleaseCheck : HealthCheckBase<NuGetReleaseCheckConfig>
    {
        public const string OfficialFeedUrl = "https://go.microsoft.com/fwlink/?LinkID=206669";
        public const string FilenameTemplate = "_{0}.check.state";
        
        public const string FeedProperty = "Feed";
        public const string PackageIdProperty = "PackageId";
        public const string PreviousVersionProperty = "PreviousVersion";
        public const string NewVersionProperty = "NewVersion";

        private string _feed;
        private NuGetPackageInfos _packageState;

        private readonly string _filename;
        private readonly DataContractSerializer _serializer;
        
        public NuGetReleaseCheck(NuGetReleaseCheckConfig config) : base(config)
        {
            _serializer = new DataContractSerializer(typeof(NuGetPackageInfos));
            _filename = SmartLocation.GetLocation(string.Format(FilenameTemplate, _config.FriendlyId));
        }

        public override void Initialise()
        {
            _feed = string.IsNullOrWhiteSpace(_config.Feed) ? OfficialFeedUrl : _config.Feed;

            // create the state file if it does not exist
            if (!File.Exists(_filename))
            {
                Logger.Info("Establishing state file for NuGetReleaseCheck ({0})...{1}", 
                    Identity.Name, _filename);

                _packageState = new NuGetPackageInfos();
                SaveState();                
            }
            else
            {
                Logger.Info("Restoring state file for NuGetReleaseCheck ({0}) from {1}", 
                    Identity.Name, _filename);

                using (var reader = new FileStream(_filename, FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    _packageState = (NuGetPackageInfos)_serializer.ReadObject(reader);
                }
            }
        }

        private void SaveState()
        {            
            using (var writer = new FileStream(_filename, FileMode.Create, FileAccess.Write, FileShare.None))
            {
                _serializer.WriteObject(writer, _packageState);
            }
        }

        public override void Execute()
        {
            Logger.Info("NuGetReleaseCheck ({0}) is checking feed '{1}' for new releases to...", 
                Identity.Name, _feed);

            var repo = PackageRepositoryFactory.Default.CreateRepository(_feed);

            _config.Packages.ForEach(
                packageId =>
                    {
                        Logger.Info("\tPackage '{0}'", packageId);

                        var packageUpdate = repo.FindPackage(packageId);

                        if (packageUpdate == null)
                        {
                            // package not available
                            var msg = string.Format("NuGet package '{0}' does not exist on feed '{1}'", packageId, _feed);
                            Logger.Info(msg);

                            Publish(NotificationRequestBuilder.For(_config.NotificationMode,
                                HealthCheckData.For(Identity, msg)
                                    .AddTag(packageId)
                                    .AddProperty(PackageIdProperty, packageId)
                                    .Failed(), UsePackageIdAsKey)
                                .Build());
                            return;
                        }

                        if (_packageState.ContainsKey(packageId))
                        {
                            if (IsNewVersion(_packageState[packageId].Version, packageUpdate.Version.Version))
                            {
                                // new version available
                                Logger.Info("\t\tNew version {0} of package {1} is available!",
                                            packageUpdate.Version.Version, packageId);

                                PublishNewPackageAvailableMessage(packageUpdate);
                                _packageState[packageId].Feed = _feed;
                                _packageState[packageId].Version = packageUpdate.Version.Version;
                            }
                        }
                        else
                        {
                            Logger.Info("Package {0} v{1} is now available!", packageUpdate.Id, packageUpdate.Version.Version);

                            // add to state before publishing message as it expects state to
                            // contain an entry for this package (previous version)
                            _packageState.Add(new NuGetPackageInfo
                                                   {
                                                       Feed = repo.Source,
                                                       Name = packageId,
                                                       Version = packageUpdate.Version.Version
                                                   });
                            PublishNewPackageAvailableMessage(packageUpdate);
                        }
                    });

            _packageState.LastChecked = DateTime.Now;
            SaveState();
        }

        private void UsePackageIdAsKey(NotificationRequest request)
        {
            request.DataKeyGenerator = evt => string.Format("{0}_{1}", _config.FriendlyId,
                evt.Properties[PackageIdProperty]);
        }

        protected override PluginDescriptor BuildIdentity()
        {
            return new PluginDescriptor
                       {
                           Name = _config.FriendlyId,
                           Description = "Checks for new NuGet packages",
                           TypeId = new Guid("57FD3DF2-2CA7-4467-A624-8E2BE19F4D35")
                       };
        }

        protected virtual void PublishNewPackageAvailableMessage(IPackage packageUpdate)
        {
            var payload = HealthCheckData.For(Identity, "NuGet package {0} v{1} is available", packageUpdate.Id,
                                              packageUpdate.Version.Version)
                .AddTag(packageUpdate.Id)
                .AddProperty(FeedProperty, _config.Feed)
                .AddProperty(PackageIdProperty, packageUpdate.Id)
                .AddProperty(PreviousVersionProperty, _packageState[packageUpdate.Id].Version.ToString())
                .AddProperty(NewVersionProperty, packageUpdate.Version.Version.ToString())
                .Succeeded();

            Publish(NotificationRequestBuilder.For(_config.NotificationMode, payload).Build());
        }

        protected virtual bool IsNewVersion(Version current, Version update)
        {
            if (current == null)
                return false;
            if (update == null)
                return false;

            Logger.Debug("\t\tComparing versions...current:={0}, update:={1}", current, update);

            var cVal = (current.Major * 1000) + (current.Minor * 100) + (current.Revision * 10) + (current.Build);
            var uVal = (update.Major * 1000) + (update.Minor * 100) + (update.Revision * 10) + (update.Build);
            return uVal > cVal;
        }
    }
}